/**
 * 
 */
package com.uxsino.simo.indicator;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.uxsino.commons.model.NeClass;
import com.uxsino.commons.utils.CycleDetector;
import com.uxsino.commons.utils.config.IResourceWalker;
import com.uxsino.commons.utils.config.PropElement;
import com.uxsino.commons.utils.config.PropJsonDocument;
import com.uxsino.commons.utils.config.PropXMLDocument;
import com.uxsino.reactorq.model.INDICATOR_TYPE;
import com.uxsino.simo.networkentity.EntityInfo;
import com.uxsino.simo.utils.ConfigLoadingContext;

/**
 * This class contains the utilities for loading indicators from XML or JSON. It keeps a {@link ConcurrentMap} mapping
 * indicatorNames to the indicators. The methods takes care of references for {@link ListIndicator} and fields for
 * {@link CompoundIndicator}. One can go through all the indicators by calling {@code iterator()}, which returns an
 * {@link Iterator<Indicator>} It also has a method {@code dumpIndicators()} to dump an array containing all the
 * {@link Indicator}s. <br />
 * <br />
 * Issue: Currently it's used in three classes {@link collector.ExecEnvironment}, {@link monitoring.AgencyEnv} and
 * {@link agency.AgencyEnvironment}, each gets a private instance of it. Thus there are three unrelated instances of
 * this class. It should be a {@literal @Singleton} and should be {@literal @Autowired} whenever it is needed.
 * 
 * No to issue here before: 
 * 1. the simo-collector-data is designed to work without spring/spring-boot stuff.
 * 2. agency, monitor or other module should get the instance of name space from the ExecEnvironment.
 * 3. in some cases, there could be more than one name space, so it is not designed to be singleton and leave the scope
 *    control for the application in which it is used, and pass it alone to related class, like {@link NSIndicatorDepo}, etc.
 *     
 */
public class IndicatorNamespace implements Iterable<Indicator> {
    private static Logger logger = LoggerFactory.getLogger(IndicatorNamespace.class);

    private static final Pattern variableNamePattern = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*");

    private ConcurrentMap<String, Indicator> indicators = new ConcurrentHashMap<>();

    // read write lock for the indicators map
    private final ReentrantReadWriteLock rwlIndicators = new ReentrantReadWriteLock();

    private final Lock rlIndicators = rwlIndicators.readLock();

    private final Lock wlIndicators = rwlIndicators.writeLock();

    public ConfigLoadingContext loadIndicators(IResourceWalker... resourceWalkers) {

        ConfigLoadingContext lctxt = new ConfigLoadingContext(IndicatorNamespace.class);
        Arrays.stream(resourceWalkers).filter(Objects::nonNull).forEach(walker -> {
            try {
                walker.forEach(url -> {
                    String srcName = url.getFile();
                    lctxt.setCurrentSource(srcName);
                    InputStream is = null;

                    try {
                        is = url.openStream();
                        loadIndicatorsFromXmlStream(is, srcName, lctxt);
                    } catch (IllegalArgumentException e) {
                        lctxt.error("", "error loading indicator");
                    } catch (IOException e) {
                        lctxt.error("", "error reading indicator file", e);
                    } finally {
                        if (is != null) {
                            try {
                                is.close();
                            } catch (IOException e) {
                            }
                        }
                    }
                });
            } catch (IOException e) {
                logger.error("error loading indicators", e);
            }
        });
        return lctxt;
    }

    public void loadCustomIndicators(ConfigLoadingContext lctxt, JsonNode inds) {
        if (inds == null || inds.size() == 0) {
            return;
        }
        if (inds.isArray()) {
            for (JsonNode jsonNode : inds) {
                loadIndicatorsFromJson(jsonNode, lctxt);
            }
        } else {
            loadIndicatorsFromJson(inds, lctxt);
        }
    }

    public void checkCircularFieldReference(ConfigLoadingContext lctxt) {
        indicators.values().stream().filter(indicator -> indicator instanceof CompoundIndicator).forEach(ind -> {
            CycleDetector<String> detector = new CycleDetector<String>(name -> {
                IIndicatorField field = ((CompoundIndicator) ind).getField(name);
                if (!(field instanceof ExprField))
                    return null;
                return ((ExprField) field).getReferredNames().iterator();
            });

            ((CompoundIndicator) ind).getFieldsCollection().forEach(fld -> {
                List<String> circle = detector.detect(fld.getName());

                if (circle != null) {
                    lctxt.locateObjectError(ind.name, " {}: circular field reference found:{}. evaluation disabled()",
                        ind.name, circle);
                    if (fld instanceof ExprField) {
                        ((ExprField) fld).disable();
                    }
                }
            });
        });

    }

    public void checkCircularIndicatorReference(ConfigLoadingContext lctxt) {
        CycleDetector<String> detector = new CycleDetector<String>(name -> {
            Indicator ind = getIndicator(name);
            if (ind != null && ind instanceof IndicatorWithRefer) {
                return ((IndicatorWithRefer) ind).getReferedIndicatorNames().iterator();
            }
            return null;
        });
        indicators.values().stream().filter(indicator -> indicator instanceof IndicatorWithRefer).forEach(ind -> {
            List<String> circle = detector.detect(ind.name);

            if (circle != null) {
                lctxt.locateObjectError(ind.name, "{}: circular indiator reference found: {}: evaluation disabled()",
                    ind.name, circle);
                if (ind instanceof IExprItem) {
                    ((IExprItem) ind).disable();
                }
            }
        });
    }

    public boolean indicatorExists(String indicatorName) {
        return indicators.containsKey(indicatorName);
    }

    public void clear() {
        indicators.clear();
    }

    public void addIndicator(Indicator indicator) {
        indicators.put(indicator.name, indicator);
    }

    private void loadIndicators(PropElement element, ConfigLoadingContext lctxt) {
        List<PropElement> nlIndicators = element.getChildElements();
        Map<String, String> defaultConf = element.getProps();
        for (PropElement eIndicator : nlIndicators) {
            loadIndicator(eIndicator, lctxt, defaultConf);
        }

    }

    public void loadIndicatorsFromXmlFile(File xmlFile, String sourceName, ConfigLoadingContext lctxt) {
        // loadIndicatorsFromXml(path.toFile());

        logger.info("load indicator file: {}", sourceName);

        PropXMLDocument doc = new PropXMLDocument(xmlFile, sourceName);

        if (doc.isValid()) {
            loadIndicators(doc.getRootElement(), lctxt);
        } else {
            lctxt.setCurrentSource(sourceName);
            lctxt.error("0", "error loading indicator file, invalid format or file not found");
        }
    }

    public void loadIndicatorsFromXmlStream(InputStream is, String sourceName, ConfigLoadingContext lctxt) {
        // loadIndicatorsFromXml(path.toFile());

        logger.info("load indicator file: {}", sourceName);

        PropXMLDocument doc = new PropXMLDocument(is, sourceName);

        if (doc.isValid()) {
            loadIndicators(doc.getRootElement(), lctxt);
        } else {
            lctxt.setCurrentSource(sourceName);
            lctxt.error("0", "error loading indicator file, invalid format or file not found");
        }
    }

    public void reloadFromJson(JsonNode node) {
        wlIndicators.lock();
        ConfigLoadingContext lctxt = new ConfigLoadingContext(IndicatorNamespace.class);
        try {
            clear();
            lctxt.setCurrentSource("internal json");
            loadIndicatorsFromJson(node, lctxt);
        } finally {
            wlIndicators.unlock();
        }
    }

    public void loadIndicatorsFromJson(JsonNode node, ConfigLoadingContext lctxt) {
        // loadIndicatorsFromXml(path.toFile());
        PropJsonDocument doc = new PropJsonDocument(node);
        List<PropElement> nlIndicators = doc.getElements();
        for (PropElement eIndicator : nlIndicators) {
            loadIndicator(eIndicator, lctxt, null);
        }

    }

    public void loadIndicator(PropElement eIndicator, ConfigLoadingContext lctxt, Map<String, String> defaultConf) {
        try {
            Indicator ind = Indicator.create(eIndicator, lctxt);
            if (defaultConf != null && !defaultConf.isEmpty()) {
                if (StringUtils.isBlank(ind.protocols) && defaultConf.containsKey("protocols")) {
                    ind.protocols = defaultConf.get("protocols");
                }
                if (StringUtils.isBlank(ind.neclass) && defaultConf.containsKey("neclass")) {
                    ind.neclass = defaultConf.get("neclass");
                }
            }
            lctxt.addObject(ind.name, eIndicator.getSourceLocation());
            if (ind != null) {
                if (indicators.containsKey(ind.name)) {
                    lctxt.error(eIndicator.getSourceLocation(), "duplicate indicator name: {}, ignored.", ind.name);
                } else {
                    indicators.put(ind.name, ind);
                }
            }
        } catch (Exception e) {
            lctxt.error(eIndicator.getSourceLocation(), " error creating indicator. {}", e);
        }
    }

    public Indicator getIndicator(String indicatorName) {
        return indicators.getOrDefault(indicatorName, null);
    }

    public List<Indicator> getIndicators(EntityInfo entityInfo) {
        List<Indicator> inds = new ArrayList<>();
        for (Indicator ind : indicators.values()) {
            if ("system_info".equals(ind.name)) {
                System.out.println();
            }
            if ("true".equals(ind.nonexec) || ind.protocols == null || ind.neclass == null) {
                continue;
            }
            boolean meet = false;
            String[] neClasses = ind.neclass.split(",");
            for (String neClass : neClasses) {
                if (neClass.equalsIgnoreCase(entityInfo.getEntityClass().id) || neClass
                    .equalsIgnoreCase(NeClass.valueOf(entityInfo.getEntityClass().id).getBaseNeClass().toString())) {
                    meet = true;
                    continue;
                }
            }
            if (meet) {
                meet = false;
            } else {
                continue;
            }
            String[] protocols = ind.protocols.split(",");
            for (String protocol : protocols) {
                if (entityInfo.protocols.containsKey(protocol.toLowerCase())) {
                    meet = true;
                }
            }
            if (meet) {
                inds.add(ind);
            }
        }
        return inds;
    }

    public IExprItem[] getAllExprItems() {
        ArrayList<IExprItem> items = new ArrayList<IExprItem>();

        for (Indicator ind : indicators.values()) {
            if (ind instanceof IExprItem) {
                items.add((IExprItem) ind);
            } else if (ind.getIndicatorType() == INDICATOR_TYPE.COMPOUND) {
                getExprFields((CompoundIndicator) ind, items);
            } else if (ind.getIndicatorType() == INDICATOR_TYPE.LIST) {
                getExprFields((ListIndicator) ind, items);
            }
        }
        return items.toArray(new IExprItem[items.size()]);
    }

    private void getExprFields(CompoundIndicator ind, ArrayList<IExprItem> items) {
        CompoundIndicator c = ind;
        Iterator<Map.Entry<String, IIndicatorField>> it = c.getFieldIterator();
        while (it.hasNext()) {
            IIndicatorField fld = it.next().getValue();
            if (fld instanceof ExprField) {
                items.add((IExprItem) fld);
            }
        }
    }

    public Iterator<Indicator> getIndicatorsIterator() {
        return indicators.values().iterator();
    }

    public ConcurrentMap<String, Indicator> getIndicators() {
        return indicators;
    }

    public void removeIndicator(String indicatorId) {
        if (!indicatorExists(indicatorId)) {
            return;
        }
        indicators.remove(indicatorId);
    }

    public List<Indicator> dumpIndicatorList() {
        rlIndicators.lock();
        ArrayList<Indicator> l;
        try {
            l = new ArrayList<Indicator>(indicators.values());
        } finally {
            rlIndicators.unlock();
        }
        return l;
    }

    public int getIndicatorCount() {
        int num = 0;
        rlIndicators.lock();
        num = indicators.size();
        rlIndicators.unlock();
        return num;
    }

    public Indicator[] dumpIndicators() {
        rlIndicators.lock();
        try {
            return indicators.values().toArray(new Indicator[indicators.size()]);
        } finally {
            rlIndicators.unlock();
        }
    }

    @Override
    public Iterator<Indicator> iterator() {
        return indicators.values().iterator();
    }

    public static boolean checkVariableName(String name) {
        return variableNamePattern.matcher(name).matches();

    }

    public Indicator[] getIndicatorsForWarnGroupNames(String[] selectorGroupNames) {
        ArrayList<Indicator> result = new ArrayList<>();

        for (Indicator ind : indicators.values()) {
            for (String gname : selectorGroupNames) {
                if (ind.warnGroups.contains(gname)) {
                    result.add(ind);
                    break;
                }
            }
        }
        return result.toArray(new Indicator[result.size()]);
    }

}
